﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// Class used by the emitted RemoteGameComponent classes. You don't need to use it directly, but it must be public to be acessible by
	/// the emitted classes.
	/// </summary>
	[Serializable]
	public sealed class RemoteGameProperty:
		IEquatable<RemoteGameProperty>
	{
		#region Static Area
			#region Fields
				internal static YieldReaderWriterLockSlim _propertiesLock;
				internal static List<RemoteGameProperty> _properties = new List<RemoteGameProperty>();
			#endregion

			#region GetAllProperties
				/// <summary>
				/// Gets a copy of all available game properties.
				/// Note that such list changes everytime a RemoteGameClient connects.
				/// </summary>
				public RemoteGameProperty[] GetAllProperties()
				{
					_propertiesLock.EnterReadLock();
					try
					{
						return _properties.ToArray();
					}
					finally
					{
						_propertiesLock.ExitReadLock();
					}
				}
			#endregion

			#region GetPropertyValueById
				/// <summary>
				/// Gets the value of a property by id.
				/// </summary>
				public static T GetPropertyValueById<T>(int id, RemoteGameComponent component)
				{
					if (component == null)
						throw new ArgumentNullException("component");

					RemoteGameProperty info;
					_propertiesLock.EnterReadLock();
					try
					{
						if (id < 0 || id >= _properties.Count)
							throw new RemoteGameException("Can't find the given property by id. Is a RemoteGameClient or Listener running?");

						info = _properties[id];
					}
					finally
					{
						_propertiesLock.ExitReadLock();
					}

					object result;
					component._componentLock.EnterReadLock();
					try
					{
						component.CheckUndisposed();

						if (component._values.TryGetValue(id, out result))
							return (T)result;
					}
					finally
					{
						component._componentLock.ExitReadLock();
					}

					return default(T);
				}
			#endregion
			#region SetPropertyValueById
				/// <summary>
				/// Sets the value of a property by id.
				/// </summary>
				public static void SetPropertyValueById<T>(int id, RemoteGameComponent component, T value)
				{
					object valueAsObject = value;
					if (object.Equals(value, default(T)))
					{
						_SetPropertyValueById(id, component, null, component.IsClientComponent);
						valueAsObject = null;
					}
					else
						_SetPropertyValueById(id, component, value, component.IsClientComponent);

					var client = component._client;
					if (client != null)
						client._GenerateNotificationIfNeeded();
				}
				internal static void _SetPropertyValueById(int id, RemoteGameComponent component, object value, bool setByClient)
				{
					if (component == null)
						throw new ArgumentNullException("component");

					RemoteGameProperty info;
					_propertiesLock.EnterReadLock();
					try
					{
						if (id < 0 || id >= _properties.Count)
							throw new RemoteGameException("Can't find the given property by id. Is a RemoteGameClient or Listener running?");

						info = _properties[id];
					}
					finally
					{
						_propertiesLock.ExitReadLock();
					}

					if (info._isClientProperty != setByClient && !component._isParticipantDisconnected)
						throw new RemoteGameException("A client property is set by the server or vice-versa.");

					object oldValueObject;
					component._componentLock.EnterWriteLock();
					try
					{
						component.CheckUndisposed();

						component._values.TryGetValue(id, out oldValueObject);

						if (object.Equals(oldValueObject, value))
							return;

						if (value == null)
							component._values.Remove(id);
						else
							component._values[id] = value;

						if (!info._isVolatile)
							component._changes[id] = value;
					}
					finally
					{
						component._componentLock.ExitWriteLock();
					}
				}
			#endregion

			#region _GetVolatileProperties
				private static YieldReaderWriterLockSlim _volatilePropertiesLock;
				private static Dictionary<Type, RemoteGameProperty[]> _volatileProperties = new Dictionary<Type, RemoteGameProperty[]>();
				internal static RemoteGameProperty[] _GetVolatileProperties(Type type)
				{
					RemoteGameProperty[] result;
					_volatilePropertiesLock.EnterReadLock();
					try
					{
						_volatileProperties.TryGetValue(type, out result);
					}
					finally
					{
						_volatilePropertiesLock.ExitReadLock();
					}

					if (result != null)
						return result;

					_volatilePropertiesLock.EnterUpgradeableLock();
					bool upgraded = false;
					try
					{
						if (_volatileProperties.TryGetValue(type, out result))
							return result;

						var volatileProperties = new List<RemoteGameProperty>();

						_propertiesLock.EnterReadLock();
						try
						{
							foreach(var property in _properties)
								if (property._isVolatile)
									if (type.IsSubclassOf(property._declaringType))
										volatileProperties.Add(property);
						}
						finally
						{
							_propertiesLock.ExitReadLock();
						}

						result = volatileProperties.ToArray();

						_volatilePropertiesLock.UpgradeToWriteLock();
						upgraded = true;
						_volatileProperties.Add(type, result);
					}
					finally
					{
						if (upgraded)
							_volatilePropertiesLock.ExitUpgradedLock();
						else
							_volatilePropertiesLock.ExitUpgradeableLock();
					}

					return result;
				}
			#endregion
			#region GetVolatileProperties
				/// <summary>
				/// Gets a collection with all volatile game properties found on the given type.
				/// </summary>
				public static ReadOnlyCollection<RemoteGameProperty> GetVolatileProperties(Type type)
				{
					if (type == null)
						throw new ArgumentNullException("type");

					if (!typeof(RemoteGameComponent).IsAssignableFrom(type))
						throw new ArgumentException("type must be a RemoteGameComponent.", "type");

					var volatileProperties = _GetVolatileProperties(type);
					var readOnlyProperties = new ReadOnlyCollection<RemoteGameProperty>(volatileProperties);
					return readOnlyProperties;
				}
			#endregion
		#endregion

		#region Constructor
			internal RemoteGameProperty(int id, Type declaringType, Type propertyType, string name, bool isClientProperty, bool isVolatile)
			{
				_id = id;
				_declaringType = declaringType;
				_propertyType = propertyType;
				_name = name;
				_isClientProperty = isClientProperty;
				_isVolatile = isVolatile;
			}
		#endregion

		#region Properties
			#region Id
				internal readonly int _id;

				/// <summary>
				/// Gets the Id of this property. Such Id is unique between all game-properties, independent of the class.
				/// </summary>
				public int Id
				{
					get
					{
						return _id;
					}
				}
			#endregion
			#region DeclaringType
				internal readonly Type _declaringType;

				/// <summary>
				/// Gets the type that declares this game property.
				/// </summary>
				public Type DeclaringType
				{
					get
					{
						return _declaringType;
					}
				}
			#endregion
			#region PropertyType
				internal readonly Type _propertyType;

				/// <summary>
				/// Gets the type of the property.
				/// </summary>
				public Type PropertyType
				{
					get
					{
						return _propertyType;
					}
				}
			#endregion
			#region Name
				internal readonly string _name;

				/// <summary>
				/// Gets the name of the property.
				/// </summary>
				public string Name
				{
					get
					{
						return _name;
					}
				}
			#endregion
			#region IsClientProperty
				internal readonly bool  _isClientProperty;

				/// <summary>
				/// Gets a value indicating if this property is changeable from the client side.
				/// </summary>
				public bool IsClientProperty
				{
					get
					{
						return _isClientProperty;
					}
				}
			#endregion
			#region IsVolatile
				internal readonly bool _isVolatile;

				/// <summary>
				/// Gets a value indicating if this property is volatile.
				/// Volatile game properties are sent again each frame, so a lost frame doesn't cause a delay until
				/// it is received, as it can be discarded.
				/// </summary>
				public bool IsVolatile
				{
					get
					{
						return _isVolatile;
					}
				}
			#endregion
		#endregion
		#region Methods
			#region Equals
				/// <summary>
				/// Compares this property info with another one.
				/// </summary>
				public bool Equals(RemoteGameProperty other)
				{
					if (other == null)
						return false;

					return _id == other._id;
				}

				/// <summary>
				/// Compares this property info with another object.
				/// </summary>
				public override bool Equals(object obj)
				{
					var other = obj as RemoteGameProperty;
					return Equals(other);
				}
			#endregion
			#region GetHashCode
				/// <summary>
				/// Gets the Id of this property as the hashcode.
				/// </summary>
				public override int GetHashCode()
				{
					return _id;
				}
			#endregion

			#region GetPropertyInfo
				/// <summary>
				/// Tries to get a real property-info based on this game property info.
				/// </summary>
				public PropertyInfo GetPropertyInfo()
				{
					var result = _declaringType.GetProperty(_name);

					if (result == null)
						throw new RemoteGameException("Can't find property " + _name + " in type " + _declaringType.FullName);

					return result;
				}
			#endregion
		#endregion
	}
}
